package xyz.scootaloo.alg.tool

import kotlin.math.max

/**
 * 表格打印工具
 * @see generate 便捷工厂方法
 *
 * @author flutterdash@qq.com
 * @since 2021/5/9 20:48
 */
class TextTable(private val table: List<List<String>>) {
    // 内容是否由纯ascii编码构成, 这个属性用于在表的所有内容都由ascii编码构成时省略一些检查以加速表的构建
    private var onlyAsciiCode = true

    // 在 buff 末尾添加一个换行符
    private fun StringBuilder.appendLineBreak() = this.append(SEP)

    // 向 buff 中追加内容, 并添加一个换行符
    private fun StringBuilder.appendWithLineBreak(content: String) =
        this.append(content).appendLineBreak()

    // 在 buff 末尾追加 [size] 个 [c]
    private fun StringBuilder.appendChars(c: Char, size: Int): StringBuilder {
        if (size == 0) return this
        for (i in 0 until size)
            this.append(c)
        return this
    }

    // 输出表格
    override fun toString(): String {
        return buildTable()
    }

    /**
     * 构建表的逻辑:
     * +------+------+-------------+
     * | col1 | col2 | col3        | 每一行内容的上一行和下一行都是相同的分隔行
     * +------+------+-------------+
     * | as   | as   | hello world | 且不同行的相同列占用的宽度都是相同的
     * +------+------+-------------+
     * | as   | as   | 1           | 每一个单元格的文本内容默认居左
     * +------+------+-------------+
     * | as   | as   |             | 假如有一个单元格是空白的, 则这个位置仍然会用空格显示
     * +------+------+-------------+
     */
    private fun buildTable(): String {
        val lens = calcLenOfColumn()         // 计算每一列需要的宽度(单位:字符)
        val splitLine = buildSplitLine(lens) // 一个行分割线
        val buff = StringBuilder()

        buff.appendWithLineBreak(splitLine)
        for (row in table) {
            buildArray(lens, row, buff).appendLineBreak()
            buff.appendWithLineBreak(splitLine)
        }

        return buff.toString()
    }

    // 输出一行分隔行
    private fun buildSplitLine(lens: IntArray): String {
        val buff = StringBuilder()
        buff.append('+')
        for (i in lens.indices) {
            buff.appendChars('-', lens[i] + 2)
            buff.append('+')
        }
        return buff.toString()
    }

    // 构建一行内容
    private fun buildArray(lens: IntArray, row: List<String>, buff: StringBuilder): StringBuilder {
        buff.append('|')
        for (column in lens.indices) {
            val currentWidth = lens[column]
            if (column >= row.size) {
                buff.appendChars(' ', currentWidth + 2).append('|')
            } else {
                buff.append(' ')
                buildCell(buff, currentWidth, row[column])
                buff.append(' ').append('|')
            }
        }
        return buff
    }

    // 返回一个int数组, 数组中每一个元素代表其下标所对应列的宽度(在屏幕上显示占用的宽度(单位字符))
    private fun calcLenOfColumn(): IntArray {
        fun checkAndSet(text: String): Int {
            val realLength = stringLen(text)
            if (realLength != text.length && !onlyAsciiCode)
                onlyAsciiCode = false
            return realLength
        }

        val lens = IntArray(table[0].size)
        val titleSize = table[0].size
        for (row in table) {
            for (column in row.indices) {
                if (column < titleSize) {
                    lens[column] = max(checkAndSet(row[column]), lens[column])
                } else {
                    break
                }
            }
        }
        return lens
    }

    /**
     * 构建一个单元格, 并把单元格的内容追加到 buff 后:
     * 原理 ->
     * 1. 每一列都已经计算出指定的宽度, 需要将文本限制到这个宽度内
     * 2. 如果文本的实际宽度小于指定宽度, 则缺少的部分用空格补位
     * 3. 如果文本的实际宽度大于指定宽度, 多出来的部分去掉, 并用省略号表示
     */
    private fun buildCell(buff: StringBuilder, width: Int, text: String) {
        val realWidth = if (onlyAsciiCode) text.length else stringLen(text)
        val diff = width - realWidth
        if (realWidth <= width) {
            buff.append(text)
            buff.appendChars(' ', diff)
        } else {
            if (width <= 3) {
                buff.append('.', width)
            } else {
                for (pos in 0 until (width - 3))
                    buff.append(text[pos])
                buff.appendChars('.', 3)
            }
        }
    }

    companion object {
        private const val SEP = '\n'

        /**
         * 计算字符串在屏幕上占用的宽度<br>
         * 计算方式:  假如是普通字符, 例如ascii码, 则占用1个位置; 假如是特殊字符, 例如中文, 则占用两个位置
         *
         * 简化计算, 只处理中文和英文
         *
         * @param s 一个字符串
         * @return 返回字符串在屏幕上占用的宽度
         */
        fun stringLen(s: String?): Int {
            if (s == null)
                return 0

            var len = s.length
            for (c in s) {
                val intValue = c.toInt()
                // 中文的在 unicode 编码中的区间
                if (intValue in 19968..40869) {
                    len += 1
                }
            }
            return len
        }

        /**
         * 构建表格的工厂方法
         * @param table 表体, 使用List<List<String>>表示一个二维表格, 每一个String都是单元格的文本内容
         * @return      返回表格字符串, 当这个字符串输出到屏幕上会显示成类似mysql样式的表格
         */
        fun generate(
            table: List<List<String>> = emptyList()
        ): String =
            TextTable(table).toString()
    }
}
